# device_manager.py
# 设备发现与远程配置客户端

import socket
import json
import threading
import time
from tkinter import *
from tkinter import ttk, messagebox
from datetime import datetime

# ==================== 配置参数 ====================
MULTICAST_GROUP = '224.0.1.100'
LISTEN_PORT = 17070
COMMAND_PORT = 17070  # 命令发往同一端口
LOCAL_IP = '192.168.10.27'  # 你的Windows本机IP

# 存储已发现设备 {sn: info}
devices = {}
devices_lock = threading.Lock()

# UDP 发送 socket
sender_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# sender_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 如果需要跨平台兼容，不绑定发送端
# ====================

def get_local_ip():
    """自动获取本机局域网IP地址"""
    try:
        # 创建一个UDP socket，连接到一个外部地址（不会真发送）
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.connect(("8.8.8.8", 80))  # Google DNS
        local_ip = sock.getsockname()[0]
        sock.close()
        return local_ip
    except Exception:
        # 失败时回退
        return "127.0.0.1"

LOCAL_IP = get_local_ip()
print(f"🌐 检测到本机IP: {LOCAL_IP}")

class DeviceManagerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("🔧 设备发现与配置工具 v1.0")
        self.root.geometry("800x600")
        self.root.resizable(True, True)

        self.create_widgets()
        self.start_listen_thread()

    def create_widgets(self):
        # === 标题 ===
        Label(self.root, text="设备发现与远程配置", font=("正楷", 16, "bold")).pack(pady=10)

        # === 设备列表 ===
        frame = Frame(self.root)
        frame.pack(fill=BOTH, expand=True, padx=10, pady=5)

        columns = ('sn', 'ip', 'name', 'last_seen')
        self.tree = ttk.Treeview(frame, columns=columns, show='headings', height=15)
        self.tree.heading('sn', text='SN 号')
        self.tree.heading('ip', text='IP 地址')
        self.tree.heading('name', text='设备名')
        self.tree.heading('last_seen', text='最后上线')

        self.tree.column('sn', width=150)
        self.tree.column('ip', width=120)
        self.tree.column('name', width=150)
        self.tree.column('last_seen', width=180)

        # 滚动条
        vsb = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
        self.tree.configure(yscrollcommand=vsb.set)
        vsb.pack(side=RIGHT, fill=Y)
        self.tree.pack(side=LEFT, fill=BOTH, expand=True)

        # === 操作按钮区 ===
        btn_frame = Frame(self.root)
        btn_frame.pack(pady=10)

        self.set_ip_btn = Button(btn_frame, text="设置静态IP", command=self.set_ip, bg="lightblue", width=12)
        self.set_ip_btn.pack(side=LEFT, padx=5)

        self.set_sn_btn = Button(btn_frame, text="修改SN", command=self.set_sn, bg="lightgreen", width=12)
        self.set_sn_btn.pack(side=LEFT, padx=5)

        self.set_sn_btn = Button(btn_frame, text="修改设备名", command=self.set_name, bg="lightyellow", width=12)
        self.set_sn_btn.pack(side=LEFT, padx=5)

        Button(btn_frame, text="刷新", command=self.refresh_list, width=8).pack(side=LEFT, padx=5)
        Button(btn_frame, text="清空日志", command=self.clear_log, width=10).pack(side=LEFT, padx=5)

        # === 日志区 ===
        log_frame = Frame(self.root)
        log_frame.pack(fill=BOTH, expand=True, padx=10, pady=5)

        Label(log_frame, text="日志输出：", font=("正楷", 10)).pack(anchor=W)

        self.log_text = Text(log_frame, height=8, bg="#f0f0f0", font=("Consolas", 9))
        self.log_text.pack(fill=BOTH, expand=True, side=LEFT)

        log_vsb = Scrollbar(log_frame, command=self.log_text.yview)
        self.log_text.configure(yscrollcommand=log_vsb.set)
        log_vsb.pack(side=RIGHT, fill=Y)

    def log(self, msg):
        timestamp = datetime.now().strftime("%H:%M:%S")
        self.log_text.insert(END, f"[{timestamp}] {msg}\n")
        self.log_text.see(END)

    def refresh_list(self):
        """手动刷新设备列表"""
        self.tree.delete(*self.tree.get_children())  # 清空当前显示
        with devices_lock:
            for sn, info in devices.items():
                self.tree.insert('', END, values=(sn, info['ip'], info['name'], info['last_seen']))
        self.log(f"📊 列表已刷新，共显示 {len(devices)} 台设备")

    def get_selected_device(self):
        selected = self.tree.selection()
        if not selected:
            messagebox.showwarning("提示", "请先选择一个设备")
            return None
        item = self.tree.item(selected[0])
        sn = item['values'][0]
        with devices_lock:
            return devices.get(sn)
        return None

    def set_sn(self):
        dev = self.get_selected_device()
        if not dev:
            return
        old_sn = dev['sn']
        new_sn = self.ask_string("修改SN", f"请输入新SN号：", initialvalue=dev['sn'], width=40)
        if new_sn is None or new_sn.strip() == "":
            return

        cmd = {
            "cmd": "set_sn",
            "sn": new_sn.strip()
        }
        if new_sn == old_sn:  # 新旧一致，不发送
            messagebox.showinfo("提示", "SN号未改变")
            return
        self.send_command(dev['ip'], cmd)
        del devices[old_sn]
        self.log(f"✅ 向 {dev['ip']} 发送修改SN命令: {new_sn}")

    def set_ip(self):
        dev = self.get_selected_device()
        if not dev:
            return
        old_ip = dev['ip']
        new_ip = self.ask_string("设置IP", f"请输入新IP地址：", initialvalue=dev['ip'], width=40)
        if not new_ip:
            return
        if not self.validate_ip(new_ip):
            messagebox.showerror("错误", "请输入有效的IP地址")
            return
        if new_ip == old_ip:  # 新旧一致，不发送
            messagebox.showinfo("提示", "IP地址未改变")
            return
        cmd = {
            "cmd": "set_ip",
            "ip": new_ip,
            "interface": "eth2"  # 根据设备实际网卡修改
        }
        self.send_command(dev['ip'], cmd)
        self.log(f"✅ 向 {dev['ip']} 发送设置IP命令: {new_ip}")

    def set_name(self):
        dev = self.get_selected_device()
        if not dev:
            return
        old_name = dev['name']
        new_name = self.ask_string("修改设备名", f"请输入新设备名：", initialvalue=dev['name'], width=40)
        if not new_name:
            return
        if new_name == old_name:  # 新旧一致，不发送
            messagebox.showinfo("提示", "设备名未改变")
            return
        cmd = {
            "cmd": "set_name",
            "name": new_name
        }
        self.send_command(dev['ip'], cmd)
        self.log(f"✅ 向 {dev['ip']} 发送修改设备名命令: {new_name}")


    # =============== 自定义大尺寸输入弹窗 ===============
    def ask_string(self, title, prompt, initialvalue="", width=30):
        """
        自定义 askstring 弹窗，支持更宽的输入框
        返回: 用户输入的字符串，或 None（取消）
        """
        dialog = Toplevel(self.root)
        dialog.title(title)
        dialog.resizable(False, False)
        dialog.geometry(f"{width * 10 + 100}x120")  # 宽度根据字符数调整
        dialog.transient(self.root)
        dialog.grab_set()  # 模态窗口

        # 居中显示
        dialog.update_idletasks()
        x = self.root.winfo_x() + (self.root.winfo_width() // 2) - (dialog.winfo_width() // 2)
        y = self.root.winfo_y() + (self.root.winfo_height() // 2) - (dialog.winfo_height() // 2)
        dialog.geometry(f"+{x}+{y}")

        result = None

        def on_ok():
            nonlocal result
            result = entry.get()
            dialog.destroy()

        def on_cancel():
            nonlocal result
            result = None
            dialog.destroy()

        # 布局
        Label(dialog, text=prompt, font=("正楷", 10)).pack(pady=5)
        entry = Entry(dialog, width=width, font=("Consolas", 10))
        entry.pack(pady=5, padx=20, fill=X)
        entry.insert(0, initialvalue)
        entry.focus()

        btn_frame = Frame(dialog)
        btn_frame.pack(pady=10)

        Button(btn_frame, text="确定", command=on_ok, width=10, bg="lightgreen").pack(side=LEFT, padx=5)
        Button(btn_frame, text="取消", command=on_cancel, width=10).pack(side=LEFT, padx=5)

        self.root.wait_window(dialog)
        return result

    def clear_log(self):
        """清空日志文本框"""
        self.log_text.delete(1.0, END)

    def validate_ip(self, ip):
        parts = ip.split('.')
        return len(parts) == 4 and all(p.isdigit() and 0 <= int(p) <= 255 for p in parts)

    def send_command(self, target_ip, command):
        try:
            # 命令发给设备的 17070 端口
            sender_sock.sendto(json.dumps(command).encode('utf-8'), (target_ip, COMMAND_PORT))
        except Exception as e:
            self.log(f"❌ 发送命令失败: {e}")

    def start_listen_thread(self):
        thread = threading.Thread(target=self.listen_multicast, daemon=True)
        thread.start()

    def listen_multicast(self):
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            sock.bind(('', LISTEN_PORT))

            mreq = socket.inet_aton(MULTICAST_GROUP) + socket.inet_aton(LOCAL_IP)
            sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
            self.log("✅ 成功加入组播组，开始监听...")

            while True:
                data, addr = sock.recvfrom(1024)
                try:
                    msg = json.loads(data.decode('utf-8'))
                    self.handle_message(msg, addr[0])
                except json.JSONDecodeError:
                    self.log(f"⚠️ 无法解析JSON: {data.decode('utf-8', errors='replace')}")
        except PermissionError:
            self.log("❌ 权限不足！请以管理员身份运行此程序。")
        except Exception as e:
            self.log(f"❌ 监听错误: {e}")

    def handle_message(self, msg, ip):
        if msg.get("type") == "discovery":
            sn = msg.get("sn", "unknown")
            name = msg.get("name", "Unknown Device")
            current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

            with devices_lock:
                devices[sn] = {
                    "sn": sn,
                    "ip": ip,
                    "name": name,
                    "last_seen": current_time
                }

            # 主线程更新UI
            # self.root.after(0, self.refresh_list)  # 如果需要自动刷新，就打开注释
            self.log(f"🔍 发现设备: {sn} | IP: {ip} | 名称: {name}")

# ==================== 主程序入口 ====================

if __name__ == '__main__':
    # 提前导入（用于 set_sn 输入框）
    from tkinter import simpledialog

    print("📌 请确保以管理员身份运行此脚本！")
    root = Tk()
    app = DeviceManagerApp(root)
    root.mainloop()